const CONNECTOR = "_";
const isFunction = (f) => f && typeof f === "function";
const { stringify, parse } = JSON;
const _getPropKey = (key, ns) => `${ns}${CONNECTOR}${key}`;
const reductPropKey = (oldkey, ns) => {
  const regExp = new RegExp(ns + CONNECTOR);
  return oldkey.replace(regExp, "");
};

const hasOwnProp = Object.prototype.hasOwnProperty;
const toString = Object.prototype.toString;

const Types = {
  Object: "Object",
  Array: "Array",
  Function: "Function",
  Symbol: "Symbol",
  Date: "Date",
  String: "String",
  BigInt: "BigInt",
  Number: "Number",
  Boolean: "Boolean",
  Null: "Null",
  Undefined: "Undefined",
  // Window: 'Window',
  // HTMLDocument: 'HTMLDocument',
  // Math: 'Math',
};

const parseData = (data, key) => {
  const { type, value } = parse(data);
  let res;
  switch (type) {
    case Types.Object:
    case Types.Array:
    case Types.Null:
    case Types.Boolean:
    case Types.Number:
    case Types.String:
      res = parse(value);
      break;
    case Types.Date:
      res = new Date(value);
      break;
    case Types.Symbol:
      res = Symbol();
      break;
    case Types.Undefined:
      res = undefined;
      break;
    case Types.Function:
      try {
        res = eval(value);
      } catch (e) {
        console.error(`Storage Error: parse property named ["${key}"] error!`);
        throw new Error(e);
      }
      break;
    case Types.BigInt:
      res = BigInt(value);
      break;
    default:
      res = null;
      break;
  }
  return res;
};
const stringifyData = (data) => {
  const type = toString.call(data).slice(8, -1);
  let res;
  switch (type) {
    case Types.Object:
    case Types.Array:
    case Types.Null:
    case Types.Boolean:
    case Types.Number:
    case Types.String:
    case Types.Date:
      res = stringify(data);
      break;
    case Types.Symbol:
      res = "Symbol()";
      break;
    case Types.Undefined:
      res = "undefined";
      break;
    case Types.Function:
    case Types.BigInt:
      res = data.toString();
      break;
    default:
      res = "";
  }
  return stringify({ type, value: res });
};

class Storage {
  static defaults = {
    beforeSet: (v) => v,
    afterSet: () => {},
    beforeGet: () => {},
    afterGet: (v) => v,
  };
  constructor(config) {
    this.namespace = config?.namespace || "";
    this.store = null;
    this.config = { ...this.defaults, ...(config?.defaults || {}) };
    this._init();
  }

  _init() {
    if (globalThis !== window || !window.localStorage) {
      throw Error("Storage Error: localStore error!");
    }
    this.store = window.localStorage;
  }

  _mergeConfig(config) {
    return { ...this.config, ...config };
  }

  _normalizeData(content) {
    return stringifyData(content);
  }

  _setContext(prop, content) {
    this.store.setItem(_getPropKey(prop, this.namespace), content);
  }

  set(prop, value, config = {}) {
    if (!this.store) throw Error(`Storage Error: set prop ${prop} error!`);

    let content = value;
    const { beforeSet, afterSet } = this._mergeConfig(config);

    /* invoke beforeSetHook */
    if (isFunction(beforeSet)) content = beforeSet(prop, value);

    /* set content */
    content = this._normalizeData(content);
    this._setContext(prop, content);

    /* invoke afterSetHook */
    if (isFunction(afterSet)) afterSet(prop, content);

    return true;
  }

  _getContext(prop) {
    let value;
    try {
      const n = _getPropKey(prop, this.namespace);
      value = parseData(this.store.getItem(n), n);
    } catch {
      value = null;
    }
    return value;
  }

  get(prop, config) {
    if (!this.store) throw Error(`Storage Error: get prop ${prop} error!`);

    const { beforeGet, afterGet } = this._mergeConfig(config);

    /* invoke beforeGetHook */
    if (isFunction(beforeGet)) beforeGet(prop);

    /* get */
    const value = this._getContext(prop);

    /* invoke aferGetHook */
    if (isFunction(afterGet)) afterGet(prop, value);

    return value;
  }

  each(executor) {
    if (!isFunction)
      throw Error("Storage Error: argument needs to be a function!");
    for (const prop in this.store) {
      if (
        hasOwnProp.call(this.store, prop) &&
        prop.startsWith(this.namespace + CONNECTOR)
      ) {
        const key = reductPropKey(prop, this.namespace);
        const value = parse(this.store.getItem(prop));
        executor.call(this, key, value);
      }
    }
  }
  /* not prefix prop */
  remove(prop) {
    if (this._get(prop) !== null) {
      this.store.removeItem(_getPropKey(prop, this.namespace));
    }
  }

  clear() {
    Object.keys(this.store)
      .map((v) => reductPropKey(v, this.namespace))
      .forEach(this.remove.bind(this));
  }
}

export default new Storage({ namespace: false });
